package service

import (
	"encoding/json"
	"fmt"
	"github.com/beego/beego/v2/server/web/context"
	"github.com/google/uuid"
	pkgerrors "github.com/pkg/errors"
	"net/http"
	"scase.io/application-auto-scaling-service/pkg/api/errors"
	"scase.io/application-auto-scaling-service/pkg/api/model"
	"scase.io/application-auto-scaling-service/pkg/api/model/cci"
	"scase.io/application-auto-scaling-service/pkg/cloudresource"
	"scase.io/application-auto-scaling-service/pkg/common"
	"scase.io/application-auto-scaling-service/pkg/db"
	"scase.io/application-auto-scaling-service/pkg/service/taskservice"
	"scase.io/application-auto-scaling-service/pkg/utils/logger"
)

type Service struct {
	Ctx     *context.Context
	tLogger *logger.FMLogger
}

func NewCreateEcsGroupService(ctx *context.Context, tLogger *logger.FMLogger) *Service {
	s := &Service{
		Ctx:     ctx,
		tLogger: tLogger,
	}
	return s
}

func (s *Service) CreatePrepaidECSGroup(projectId string, req model.CreateScalingGroupReq) (*model.CreateScalingGroupResp, *errors.ErrorResp) {
	if err := db.AddOrUpdateAgencyInfo(getAgencyInfo(req, projectId)); err != nil {
		s.tLogger.Error("Add or update AgencyInfo to db err: %+v", err)
		return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, err.Error())
	}

	// 1. create group
	ecsgroup, err := s.addScalingEcsGroup(*req.Agency.OriProjectId, req)
	if err != nil {
		s.tLogger.Error("Add scaling group info err: %+v", err)
		return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, err.Error())
	}
	s.tLogger.Info("create ecs group success, group id:%s", ecsgroup.Id)
	// 2. create ecs
	taskErr := taskservice.StartEcsScalingOutTask(ecsgroup, req.DesireInstanceNumber)
	if taskErr != nil {
		if !pkgerrors.Is(taskErr, common.ErrNoNeedToDo) {
			s.tLogger.Error("StartEcsScalingOutTask error:%+v", taskErr)
			return nil, errors.NewErrorRespWithMessage(errors.ServerInternalError, taskErr.Error())
		}
	}
	createResponse := model.CreateScalingGroupResp{}
	createResponse.ScalingGroupId = ecsgroup.Id
	return &createResponse, nil
}

// 更新伸缩组，只支持修改min max desNum， cooldowntime，enableAutoScale,instanceConfig，tag
func (s *Service) UpdatePrePaidEcsGroup(projectId, groupId string, req model.UpdateScalingGroupReq) error {
	if exist := db.IsScalingGroupExist(projectId, groupId); !exist {
		s.tLogger.Error("The scaling group[%s] of project[%s] is not found", groupId, projectId)
		return fmt.Errorf("scaling [%s] group not found", groupId)
	}
	rc, err := cloudresource.GetEcsController(projectId)
	if err != nil {
		s.tLogger.Error("Get resource controller of project[%s] err: %+v", projectId, err)
		return err
	}
	group, err := db.GetScalingGroupById(projectId, groupId)
	if err != nil {
		s.tLogger.Error("Read scaling group[%s] from db err: %+v", groupId, err)
		return err
	}
	// 从db读出来之后的状态A，在此函数结尾写入db时，决策扩缩容时的状态B会被A覆盖，因此记录修改的col
	cols := []string{}
	// 更新配置到DB和Tag到Ecs
	if err := s.UpdateScalingGroupConfig(rc, req, group, &cols); err != nil {
		s.tLogger.Error("UpdateScalingGroupTagsAndInstanceConfiguration error:%+v", err)
		return err
	}
	// 修正desNum，扩缩容决策
	if err := s.UpdateInstanceNumberParam(req, group, &cols); err != nil {
		s.tLogger.Error("update instance number param error:%+v", err)
		return err
	}
	if err := s.JudgeScalingNum(group, group.DesireInstanceNumber, &cols); err != nil {
		s.tLogger.Error("judge scaling number error:%+v", err)
		return err
	}

	// 将group信息update到数据库，指定col是为了防止修改state时的并发冲突
	if err = db.UpdateScalingGroup(group, cols...); err != nil {
		s.tLogger.Error("Update scaling group[%s] to db err: %+v", group.Id, err)
		return err
	}

	return nil
}

// group通过指针引用接收修改
func (s *Service) UpdateScalingGroupConfig(rc *cloudresource.EcsResourceController, req model.UpdateScalingGroupReq,
	group *db.ScalingGroup, cols *[]string) error {
	// 更新instanceConfig
	if req.InstanceConfiguration != nil {
		instanceConfig, err := updateInstanceConfiguration(*req.InstanceConfiguration, group.InstanceConfiguration)
		if err != nil {
			s.tLogger.Error("Update instance configuration err: %+v", err)
			return err
		}
		if err := db.UpdateInstanceConfiguration(instanceConfig); err != nil {
			s.tLogger.Error("Update instance config error:%+v", err)
			return err
		}
	}
	// 开启、关闭自动伸缩，允许自动伸缩需要先绑定策略
	if req.EnableAutoScaling != nil {
		exist := db.GroupPolicyRelationTable().CheckRelationExist(group.Id, "")
		if !exist && *req.EnableAutoScaling {
			s.tLogger.Error(errors.AutoScalingUpdateError.Msg())
			return fmt.Errorf("auto scaling need at least one scaling policy")
		}
		*cols = append(*cols, "enable_auto_scaling")
		group.EnableAutoScaling = *req.EnableAutoScaling
	}
	if req.CoolDownTime != nil {
		group.CoolDownTime = int64(*req.CoolDownTime)
		*cols = append(*cols, "cool_down_time")
	}

	// 更新tag到db，只对新创的实例有效
	if req.InstanceTags != nil {
		tagsByte, err := json.Marshal(req.InstanceTags)
		if err != nil {
			s.tLogger.Error("Marshal instance tags: %+v failed, err: %+v", req.InstanceTags, err)
			return err
		}
		group.InstanceTags = string(tagsByte)
		*cols = append(*cols, "instance_tags")
	}
	return nil
}

func (s *Service) DeleteScalingGroup(log *logger.FMLogger, projectId, groupId string) *errors.ErrorResp {
	if !db.IsScalingGroupExist(projectId, groupId) {
		log.Info("The scaling group[%s] of project[%s] is not found", groupId, projectId)
		return nil
	}
	if err := taskservice.StartDeleteEcsScaleGroupTask(groupId); err != nil {
		// 任务已存在，直接返回
		if pkgerrors.Is(err, common.ErrDeleteTaskAlreadyExists) {
			log.Info("Delete task for group[%s] is running, do nothing", groupId)
			return nil
		}
		log.Error("Start delete scaling group task for group[%s] err: %+v", groupId, err)
		return errors.NewErrorRespWithHttpCode(errors.ScalingGroupNotFound, http.StatusInternalServerError)
	}

	return nil
}

// 决策扩容，缩容
// 考虑伸缩组正在扩容中，ecs还未上报，此时再决策会造成重复扩容，因此扩容的数量应该要考虑异步任务中正在扩容的个数
// 正在扩容时，伸缩组还未稳定，不会缩容，因此只要在扩容时考虑
func (s *Service) JudgeScalingNum(ecsGroup *db.ScalingGroup, targetNum int32, cols *[]string) error {
	s.tLogger.Info("group [%s] current instance num:%d, desire num:%d, new target num:%d ",
		ecsGroup.Id, ecsGroup.CurrentInstanceNumber, ecsGroup.DesireInstanceNumber, targetNum)

	if ecsGroup.CurrentInstanceNumber > targetNum {
		s.tLogger.Info("group [%s] current num:%d > target num:%d, will scaling in",
			ecsGroup.Id, ecsGroup.CurrentInstanceNumber, targetNum)
		// 启动缩容任务
		if err := taskservice.StartManualScaleInTask(ecsGroup, ecsGroup.CurrentInstanceNumber-targetNum); err != nil {
			s.tLogger.Error("start scale in task error:%s", err.Error())
			return err
		}
		return nil
	}
	if ecsGroup.CurrentInstanceNumber < targetNum {
		if err := taskservice.StartEcsScalingOutTask(ecsGroup, targetNum); err != nil {
			if !pkgerrors.Is(err, common.ErrNoNeedToDo) {
				s.tLogger.Error("start scaling out task error:%+v", err)
				return err
			}
		}
		return nil
	}
	return nil
}

func (s *Service) ListInstance(fleetId string) (interface{}, error) {
	groupId, err := db.ScalingGroupTable().GetGroupIdByFleetId(fleetId)
	if err != nil {
		s.tLogger.Error("get group id by fleet id [%s] error:+v", fleetId, err)
		return nil, err
	}
	group, err := db.ScalingGroupTable().GetScalingGroupById(groupId)
	if err != nil {
		s.tLogger.Error("get group [%s] error:+v", groupId, err)
		return nil, err
	}
	limit := common.GetLimit(s.Ctx, s.tLogger)
	offset := common.GetStartNumber(s.Ctx, s.tLogger)
	state := s.Ctx.Input.Query(common.ParamHealthState)
	if group.InstanceType == common.InstanceTypeVM {
		return s.ListVMInstance(groupId, state, limit, offset)
	} else if group.InstanceType == common.InstanceTypePod {
		return s.ListPodInstance(groupId, state, limit, offset)
	} else {
		s.tLogger.Error("invalid instance type: %s", group.InstanceType)
		return nil, fmt.Errorf("invalid instance type: %s", group.InstanceType)
	}
	return nil, fmt.Errorf("invalid instance type:%s", group.InstanceType)
}

func (s *Service) ListVMInstance(groupId, state string, limit, offset int) (*model.ListInstanceResponse, error) {
	count, total, ecsList, err := db.EcsInfoTable().ListInstance(groupId, state, limit, offset)
	if err != nil {
		s.tLogger.Error("list group [%s] VM instance error:%+v", groupId, err)
		return nil, err
	}
	instanceList := []model.InstanceInfo{}
	for _, instance := range ecsList {
		ecsInfo := model.InstanceInfo{
			Name:         instance.Name,
			InstanceId:   instance.InstanceId,
			GroupId:      instance.GroupId,
			HealthStatus: instance.State,
			CreatedAt:    instance.CreateAt.Format(common.TimeLayout),
		}
		if !instance.ActiveTime.IsZero() {
			ecsInfo.ActiveAt = instance.ActiveTime.Format(common.TimeLayout)
		}
		instanceList = append(instanceList, ecsInfo)
	}
	listInstanceResponse := model.ListInstanceResponse{
		Count:       count,
		TotalNumber: total,
		Instances:   instanceList,
	}
	return &listInstanceResponse, nil
}

func (s *Service) ListPodInstance(groupId, state string, limit, offset int) (*cci.ListPodResponse, error) {
	count, total, podListDB, err := db.PodInfoTable().ListPod(groupId, state, limit, offset)
	if err != nil {
		s.tLogger.Error("list group [%s] pod instance error:%+v", groupId, err)
		return nil, err
	}
	PodInstanceList := []cci.PodInfo{}
	for _, pod := range podListDB {
		podinfo := cci.PodInfo{
			Name:         pod.HostName,
			InstanceId:   pod.InstanceId,
			GroupId:      pod.ScalingGroupId,
			HealthStatus: pod.State,
			PublicIP:     pod.PublicIp,
			PrivateIp:    pod.PrivateIp,
			CreatedAt:    pod.CreateAt.Format(common.TimeLayout),
		}
		if !pod.ActiveTime.IsZero() {
			podinfo.ActiveAt = pod.ActiveTime.Format(common.TimeLayout)
		}
		PodInstanceList = append(PodInstanceList, podinfo)
	}
	listInstanceResponse := cci.ListPodResponse{
		Count:       count,
		TotalNumber: total,
		Instances:   PodInstanceList,
	}
	return &listInstanceResponse, nil
}

func (s *Service) UpdateInstanceNumberParam(req model.UpdateScalingGroupReq, group *db.ScalingGroup, cols *[]string) error {
	originDes := group.DesireInstanceNumber
	if req.MaxInstanceNumber != nil {
		group.MaxInstanceNumber = *req.MaxInstanceNumber
		if group.DesireInstanceNumber > *req.MaxInstanceNumber {
			group.DesireInstanceNumber = *req.MaxInstanceNumber
		}
		*cols = append(*cols, "max_instance_number")
	}
	if req.MinInstanceNumber != nil {
		group.MinInstanceNumber = *req.MinInstanceNumber
		if group.DesireInstanceNumber < *req.MinInstanceNumber {
			group.DesireInstanceNumber = *req.MinInstanceNumber
		}
		*cols = append(*cols, "min_instance_number")
	}
	if req.DesireInstanceNumber != nil {
		group.DesireInstanceNumber = *req.DesireInstanceNumber
		*cols = append(*cols, "desire_instance_number")
	}
	if group.MaxInstanceNumber < group.MinInstanceNumber || group.DesireInstanceNumber < group.MinInstanceNumber ||
		group.DesireInstanceNumber > group.MaxInstanceNumber {
		return fmt.Errorf("invalid instance number, do not match minimun:%d <= desire:%d <= maximun:%d",
			group.MinInstanceNumber, group.DesireInstanceNumber, group.MaxInstanceNumber)
	}
	if originDes != group.DesireInstanceNumber {
		*cols = append(*cols, "desire_instance_number")
	}
	return nil
}

func (s *Service) addScalingEcsGroup(oriProjectId string, req model.CreateScalingGroupReq) (*db.ScalingGroup, error) {
	ic, err := getNewInstanceConfiguration(*req.InstanceConfiguration, uuid.NewString())
	if err != nil {
		s.tLogger.Error("getNewInstanceConfiguration error:%s", err.Error())
		return nil, err
	}
	if err = db.AddInstanceConfiguration(ic); err != nil {
		s.tLogger.Error("AddInstanceConfiguration error:%s", err.Error())
		return nil, err
	}
	group, err := convertCreateScalingGroupReq(req, oriProjectId, uuid.NewString(), uuid.NewString(), ic)
	if err != nil {
		s.tLogger.Error("convertCreateScalingGroup error:%s", err.Error())
		return nil, err
	}
	if err = db.ScalingGroupTable().AddNewScalingGroup(group); err != nil {
		return nil, err
	}

	s.tLogger.Info("insert scaling ecs group success, ecs group:%+v", group)
	return group, nil
}
